package com.example.mq_sm.mqserver.datacenter;

import com.example.mq_sm.common.BinaryTool;
import com.example.mq_sm.common.MQException;
import com.example.mq_sm.mqserver.core.MSGQueue;
import com.example.mq_sm.mqserver.core.Message;

import java.io.*;
import java.util.LinkedList;
import java.util.Scanner;

public class MessageFileManager {
    public static class Stat{
        public int totalCount;
        public int validCount;
    }

    public void init(){}

    public String getQueueDir(String queueName){
        return "./data/" + queueName;
    }
    public String getQueueDataPath(String queueName){
        return getQueueDir(queueName) + "/queue_data.txt";
    }
    public String getQueueStatPath(String queueName){
        return getQueueDir(queueName) + "/queue_stat.txt";
    }

    private Stat readStat(String queueName){
        Stat stat = new Stat();
        try (InputStream inputStream = new FileInputStream(getQueueStatPath(queueName))){
            Scanner scanner = new Scanner(inputStream);
            stat.totalCount = scanner.nextInt();
            stat.validCount = scanner.nextInt();
            return stat;
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    private void writeStat(String queueName, Stat stat){
        try {
            OutputStream outputStream = new FileOutputStream(getQueueStatPath(queueName));
            PrintWriter printWriter = new PrintWriter(outputStream);
            printWriter.write(stat.totalCount + "\t" +stat.validCount);
            printWriter.flush();
            printWriter.close(); //需要关闭写入，否则后面删除文件的时候，文件被占用，无法删除
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    // 创建队列对应的文件和目录
    public void createQueueFiles(String queueName) throws IOException {
        // 先判断目录是否存在
        File baseDir = new File(getQueueDir(queueName));
        if (!baseDir.exists()){
            baseDir.mkdirs();
        }

        // 创建队列数据文件
        File queueDataFile = new File(getQueueDataPath(queueName));
        if (!queueDataFile.exists()){
            boolean flag = queueDataFile.createNewFile();
            if (!flag){
                throw new IOException("创建文件失败！queueDataFile=" + queueDataFile.getAbsolutePath());
            }
        }

        // 创建消息统计文件
        File queueStatFile = new File(getQueueDataPath(queueName));
        if(!queueStatFile.exists()){
            boolean flag = queueStatFile.createNewFile();
            if (!flag){
                throw new IOException("创建文件失败！queueStatFile=" + queueStatFile.getAbsolutePath());
            }
        }

        // 给消息统计文件设定一个初始值。0\t0
        Stat stat = new Stat();
        stat.totalCount = 0;
        stat.validCount = 0;
        writeStat(queueName, stat);

    }

    // 删除队列的目录和文件
    // 队列也是可以删除的 当队列删除之后 对应的消息文件等也要随之删除
    public void destroyQueueFiles(String queueName) throws IOException {
        File file1 = new File(getQueueDataPath(queueName));
        boolean ok1 = file1.delete();

        File file2 = new File(getQueueStatPath(queueName));
        System.out.println(file2.exists());
        boolean ok2 = file2.delete();
        System.out.println(ok2);

        File dir = new File(getQueueDir(queueName));
        boolean ok3 = dir.delete();
        if (ok1 && ok2 && ok3){

        }else{
            throw new IOException("删除失败！baseDir=" + dir.getAbsolutePath());
        }
    }

    // 检查队列的目录和文件是否存在
    public boolean checkFilesExist(String queueName){
        // 判断队列的数据文件和统计文件是否都存在
        File file1 = new File(getQueueDataPath(queueName));
        if (!file1.exists()){
            return false;
        }

        File file2 = new File(getQueueStatPath(queueName));
        if (!file2.exists()){
            return false;
        }
        return true;
    }

    // 向文件中发送消息
    public void sendMessage(MSGQueue queue, Message message) throws IOException, MQException {
        // 检查一下当前写入的队列对应的文件是否存在
        //System.out.println(queue.getName());
        if (!checkFilesExist(queue.getName())){
            throw new MQException("[MessageFileManager] 队列对应的文件不存在！queueName=" + queue.getName());
        }
        byte[] messageBinary = BinaryTool.toBytes(message);
        synchronized (queue){
            try(OutputStream outputStream = new FileOutputStream(getQueueDataPath(queue.getName()), true)){
                try(DataOutputStream dataOutputStream = new DataOutputStream(outputStream)){
                    long length = new File(getQueueDataPath(queue.getName())).length();
                    dataOutputStream.writeInt(messageBinary.length);
                    dataOutputStream.write(messageBinary);
                    message.setOffsetBeg(length + 4);
                    message.setOffsetEnd(length + 4 + messageBinary.length);
                }
            }
            Stat stat = readStat(queue.getName());
            stat.validCount += 1;
            stat.totalCount += 1;
            writeStat(queue.getName(), stat);
        }
    }

    // 删除文件当中的消息
    // 这里的删除是逻辑删除
    public void deleteMessage(MSGQueue queue,Message message) throws IOException, ClassNotFoundException {
        synchronized (queue) {
            try (RandomAccessFile randomAccessFile = new RandomAccessFile(getQueueDataPath(queue.getName()), "rw")) {
                //System.out.println(message.getOffsetBeg() + "-" + message.getOffsetEnd() + "\n");
                byte[] tmp = new byte[(int)(message.getOffsetEnd()-message.getOffsetBeg())];
                randomAccessFile.seek(message.getOffsetBeg());
                randomAccessFile.read(tmp);
                Message diskMessage = (Message) BinaryTool.fromBytes(tmp);
                diskMessage.setIsValid((byte) 0x0);
                //此处不需要给参数的message的isValid设为0 ，此处这个参数代表的是内存中管理的Message对象
                //而这个对象马上也要从内存中销毁
                //重新写回文件
                byte[] bufferDest=BinaryTool.toBytes(diskMessage);
                //虽然之前seek过了，但是上面seek完了之后，进行了读操作，这一读就导致光标往后移动，移动到下一个消息的位置
                //因此想要让接下来的写入,能够刚好写回到之前的位置,就需要重新调整文件光标
                randomAccessFile.seek(message.getOffsetBeg());
                randomAccessFile.write(bufferDest);
            }
            //更新统计文件  isValid-1
            Stat stat=readStat(queue.getName());
            if (stat.validCount>0){
                stat.validCount-=1;
            }
            writeStat(queue.getName(),stat);
        }
    }
    // 将硬盘中的消息全部复原
    // 这个方法准备在程序启动时进行调用
    // 这里使用一个 LinkedList，主要目的是为了后续进行头删操作
    // 该方法是在程序启动时调用，此时服务器还不能处理请求，不涉及多线程操作文件
    public LinkedList<Message> loadAllMessageFromQueue(String queueName) throws IOException, ClassNotFoundException, MQException {
        LinkedList<Message> messages = new LinkedList<>();
        try(InputStream inputStream = new FileInputStream(getQueueDataPath(queueName))){
            try(DataInputStream dataInputStream = new DataInputStream(inputStream)){
                // 这个变量记录当前文件光标
                long currentOffset = 0;
                while(true){
                    // 读取当前信息的长度，这里的 readInt 可能会读到文件末尾（EOF）
                    // readInt 方法，读到文件末尾会抛出EOFException 异常
                    int messageSize = dataInputStream.readInt();
                    byte[] messageBytes = new byte[messageSize];
                    int actualSize = dataInputStream.read(messageBytes);
                    if (messageSize != actualSize){
                        // 如果不匹配，说明文件有问题，格式错乱了
                        // 这里的格式错乱大多数指这个消息在存到文件中的时候，存到一半文件存满了
                        throw new MQException("[MessageFileManager] 文件格式错误！queueName=" + queueName);
                    }
                    Message message = (Message) BinaryTool.fromBytes(messageBytes);
                    if (message.getIsValid() != 0x1){
                        // 无效数据跳过，但要更新下标
                        currentOffset += (4 + messageSize);
                        continue;
                    }
                    // 使用 DataInputStream 需要手动计算文件光标
                    message.setOffsetBeg(currentOffset + 4);
                    message.setOffsetEnd(currentOffset + 4 + messageSize);
                    currentOffset += (4 + messageSize);
                    messages.add(message);
                }
            }catch (EOFException e){
                System.out.println("[MessageFileManager] 恢复 Message 数据完成！");
            }
        }
        return messages;
    }

    // 先写一个检查是否需要 gc 的方法
    public boolean checkGC(String queueName){
        Stat stat = readStat(queueName);
        if (stat.totalCount >= 2000 && (double)stat.validCount / (double)stat.totalCount > 0.5){
            return true;
        }
        return false;
    }

    // 需要写一个私有方法辅助 gc 过程，这里写一个获取新目录的路径的方法
    private String getNewQueueDataPath(String queueName){
        return getQueueDir(queueName) + "/queue_data_new.txt";
    }

    // 接下来开始实现 gc
    // 然后将有效数据拷贝到新的文件当中
    // 拷贝完成后删除旧的文件，并将新文件进行重命名
    // 更改 stat 文件中内容
    public void gc(MSGQueue queue) throws MQException, IOException, ClassNotFoundException {
        synchronized (queue){
            // 由于 gc 的时候 是针对消息数据文件进行大洗牌 在这个过程中 其他线程不能针对该队列的消息文件做任何修改
            // 由于 gc 操作时间可能比较长，所以统计一下所用时间
            long gcBeg = System.currentTimeMillis();

            LinkedList<Message> messages = loadAllMessageFromQueue(queue.getName());
            File newFile = new File(getNewQueueDataPath(queue.getName()));
            if (newFile.exists()){
                throw new MQException("[MessageFileManager] gc 时发现该队列的 queue_data_new 已经存在！queueName=" + queue.getName());
            }
            boolean flag2 = newFile.createNewFile();
            if (!flag2){
                throw new MQException("[MessageFileManager] 创建文件失败！queueDataNewFile=" + newFile.getAbsolutePath());
            }
            try (OutputStream outputStream = new FileOutputStream(newFile)){
                try (DataOutputStream dataOutputStream = new DataOutputStream(outputStream)){
                    for (Message message : messages){
                        byte[] buffer = BinaryTool.toBytes(message);
                        dataOutputStream.writeInt(buffer.length);
                        dataOutputStream.write(buffer);
                    }
                }
            }
            File oldFile = new File(getQueueDataPath(queue.getName()));
            boolean flag3 = oldFile.delete();
            if (!flag3){
                throw new MQException("[MessageFileManager] 删除旧的数据文件失败！queueDataOldFile" + oldFile.getAbsolutePath());
            }

            // 对新文件进行一个重命名
            flag3 = newFile.renameTo(oldFile);
            if (!flag3){
                throw new MQException("[MessageFileManager] 文件重命名失败！queueDataOldFile" + oldFile.getAbsolutePath()
                    + ", queueDataNewFile=" + newFile.getAbsolutePath());
            }

            // 更新统计文件
            Stat stat = new Stat();
            stat.totalCount = messages.size();
            stat.validCount = messages.size();
            writeStat(queue.getName(), stat);

            long gcEnd = System.currentTimeMillis();
            System.out.println("[MessageFileManager] gc 执行完毕！queueName=" + queue.getName() + ", time="
                + (gcEnd - gcBeg) + "ms");
        }

    }
}

















